﻿#region [===== Using =====]
using System;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.Workflow;
using System.ComponentModel;
using System.Globalization;
using System.Security.Permissions;
#endregion

namespace TPG.SharePoint.Workflow
{
    /// <summary>
    /// A base page for workflow association forms.
    /// </summary>
    /// <remarks>
    /// <para>This class implements the generic framework code for handling workflow
    /// associations. The following tasks are performed by this class.</para>
    /// <list type="bullet">
    ///     <item>Creating task and history lists</item>
    ///     <item>Creating and updating workflow association objects</item>
    /// </list>
    /// <para>An <see cref="AssociationPage" /> is an ASPX page displayed 
    /// during workflow association. As the implementer of workflow association 
    /// forms you can derive from this class to implement the generic aspects of the
    /// workflow form. The non-generic data contained in the form is available as a
    /// <see cref="ConfigurationData" /> object. Using the
    /// <see cref="AssociationPage.CommitAssociation" /> method you commit the 
    /// association and provide custom data for the workflow. The 
    /// <see cref="AssociationPage.CancelAssociation" /> method redirects the 
    /// user back to the workflow settings page without storing any data and 
    /// without performing the association. Existing association data can be 
    /// retrieved during updates of a workflow association using the 
    /// <see cref="AssociationPage.GetAssociationData" /> method.
    /// </remarks>
    public abstract class AssociationPage
        : LayoutWorkflowPage
    {
        #region [===== Inner classes =====]
        enum AssociationType
        {
            List,
            ContentType,
            ListContentType
        }

        class OnOffConverter : BooleanConverter
        {
            public override object ConvertFrom(
                ITypeDescriptorContext context, 
                CultureInfo culture, 
                object value)
            {
                string valueString = value as string;
                if (valueString != null)
                {
                    if (String.IsNullOrEmpty(valueString))
                    {
                        return false;
                    }
                    if (String.Equals(valueString, "on",
                        StringComparison.OrdinalIgnoreCase))
                    {
                        return true;
                    }
                    if (String.Equals(valueString, "off",
                        StringComparison.OrdinalIgnoreCase))
                    {
                        return false;
                    }
                }
                return base.ConvertFrom(context, culture, value);
            }
        }
        #endregion
        
        #region [===== Instance fields =====]
        bool? _isInitialLoad;
        SPContentType _contentType;
        SPContentType _listContentType;
        #endregion

        #region [===== Properties =====]

        #region [===== Public state =====]
        /// <summary>
        /// Gets the workflow <see cref="PageType" />, indicating the type 
        /// of the workflow page.
        /// </summary>
        public override sealed PageType PageType
        {
            get { return PageType.Association; }
        }
        /// <summary>
        /// Gets a value indicating whether the form is opened
        /// for the first time.
        /// </summary>
        /// <remarks>
        public sealed override bool IsInitialLoad
        {
            get 
            {
                return _isInitialLoad ?? true;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this association form is used
        /// to create a new association or edit an existing one.
        /// </summary>
        public bool IsNewAssociation
        {
            get { return ExistingAssociationID == null; }
        }

        /// <summary>
        /// Gets the permissions required to view the page.
        /// </summary>
        /// <remarks>
        /// This method returns a different value depending on the association
        /// type. 
        /// <list type="table">
        ///     <item>
        ///         <term>List</term>
        ///         <description>SPBasePermissions.ManageLists</description>
        ///     </item>
        ///     <item>
        ///         <term>ListContentType</term>
        ///         <description>SPBasePermissions.ManageLists</description>
        ///     </item>
        ///     <item>
        ///         <term>ContentType</term>
        ///         <description>SPBasePermissions.AddAndCustomizePages</description>
        ///     </item>
        /// </list>
        /// </remarks>
        protected override SPBasePermissions RightsRequired
        {
            get
            {
                SPBasePermissions rightsRequired;
                if (WorkflowAssociationType == AssociationType.List ||
                    WorkflowAssociationType == AssociationType.ListContentType)
                {
                    rightsRequired = (SPBasePermissions.EmptyMask | SPBasePermissions.ManageLists);
                }
                else
                {
                    rightsRequired = (SPBasePermissions.EmptyMask | SPBasePermissions.AddAndCustomizePages);
                }
                return rightsRequired;
            }
        }

        #endregion

        #region [===== Query-string Parameters =====]
        /// <summary>
        /// Gets the name of the workflow as specified on the first form
        /// displayed to the end-user during workflow association.
        /// </summary>
        protected string WorkflowName
        {
            get { return QueryStringParser.GetParameter<String>("WorkflowName"); }
        }
        
        Guid WorkflowTemplateID
        {
            get { return QueryStringParser.GetParameter<Guid>("WorkflowDefinition"); }
        }
        
        Guid? ExistingAssociationID
        {
            get { return QueryStringParser.GetParameterAllowNull<Guid?>("GuidAssoc"); }
        }
        
        Guid? ListID
        {
            get { return QueryStringParser.GetParameterAllowNull<Guid?>("List"); }
        }
        
        string ContentTypeID
        {
            get { return QueryStringParser.GetParameterAllowNull<String>("ctype"); }
        }
        
        AssociationType WorkflowAssociationType
        {
            get
            {
                AssociationType type = AssociationType.List;
                Guid? listID = ListID;
                string contentTypeID = ContentTypeID;
                if (String.IsNullOrEmpty(contentTypeID) == false &&
                    listID != null)
                {
                    type = AssociationType.ListContentType;
                }
                else if (String.IsNullOrEmpty(contentTypeID) &&
                    listID != null)
                {
                    type = AssociationType.List;
                }
                else if (String.IsNullOrEmpty(contentTypeID) == false &&
                    listID == null)
                {
                    type = AssociationType.ContentType;
                }
                else
                {
                    throw new SPException("Association type could not be determined");
                }
                return type;
            }
        }
        
        bool AutoStartWorkflowOnCreate
        {
            get
            {
                return QueryStringParser.GetParameter<bool>(
                    "AutoStartCreate", true, new OnOffConverter());
            }
        }
        
        bool AutoStartWorkflowOnChange
        {
            get
            {
                return QueryStringParser.GetParameter<bool>(
                    "AutoStartChange", true, new OnOffConverter());
            }
        }
        
        bool ManualStartWorkflow
        {
            get
            {
                return QueryStringParser.GetParameter<bool>(
                    "AllowManual", true, new OnOffConverter());
            }
        }
        
        string TaskListID
        {
            get { return QueryStringParser.GetParameter<String>("TaskList");}
        }        
        
        string HistoryListID
        {
            get { return QueryStringParser.GetParameter<String>("HistoryList");}
        }
        
        bool CascadeContentTypeToList
        {
            get { return QueryStringParser.GetParameter<bool>("UpdateLists");}
        }
        #endregion

        #region [===== Workflow association target cache =====]
        SPContentType SiteContentType
        {
            get
            {
                if (_contentType == null && WorkflowAssociationType == AssociationType.ContentType)
                {
                    SPContentTypeId contentTypeID = new SPContentTypeId(ContentTypeID);
                    _contentType = Web.AvailableContentTypes[contentTypeID];
                }
                return _contentType;
            }
        }

        SPContentType ListContentType
        {
            get
            {
                if (_listContentType == null && WorkflowAssociationType == AssociationType.ListContentType)
                {
                    SPList list = Web.Lists[ListID.Value];
                    SPContentTypeId contentTypeID = new SPContentTypeId(ContentTypeID);
                    _listContentType = list.ContentTypes[contentTypeID];
                }
                return _listContentType;
            }
        }
        #endregion

        #endregion

        #region [===== Constructor =====]
        /// <summary>
        /// Initializes a new instance of the <see cref="AssociationPage" /> 
        /// class.
        /// </summary>
        protected AssociationPage()
        {
        }
        #endregion 
        
        #region [===== Public methods =====]
        /// <summary>
        /// Supresses validation on the first load of the page.
        /// </summary>
        /// <remarks>Since the Association page is first visited as a postback, 
        /// validation fires immediatly on page load.  To supress this initial
        /// validation, the base class's implementation of validate is only 
        /// called on postbacks after the initial load.</remarks>
        public override void Validate()
        {
            if (!IsInitialLoad)
                base.Validate();
        }

        /// <summary>
        /// Supresses validation on the first load of the page.
        /// </summary>
        /// <remarks>Since the Association page is first visited as a postback, 
        /// validation fires immediatly on page load.  To supress this initial 
        /// validation, the base class's implementation of validate is only 
        /// called on postbacks after the initial load.</remarks>
        public override void Validate(string validationGroup)
        {
            if (!IsInitialLoad)
                base.Validate(validationGroup);
        }
        #endregion

        #region [===== Internal methods =====]
        internal override SPWorkflowTemplate GetWorkflowTemplate()
        {
            return Web.WorkflowTemplates[WorkflowTemplateID];
        }

        internal override SPWorkflowAssociation GetWorkflowAssociation()
        {
            SPWorkflowAssociation association = null;
            if (ExistingAssociationID != null)
            {
                association = GetExistingAssociation();
            }
            return association;
        }
        #endregion

        #region [===== Protected methods =====]

        #region [===== Page lifecycle =====]
        /// <summary>
        /// Registers the page as a control state container.
        /// </summary>
        /// <param name="e">An EventArgs containing event data.</param>
        [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")]
        protected override void OnInit(EventArgs e)
        {
            RegisterRequiresControlState(this);
            base.OnInit(e);
        }

        /// <summary>
        /// Loads the appropriate state for the page.
        /// </summary>
        /// <param name="savedState">The saved data.</param>
        protected override void LoadControlState(object savedState)
        {
            _isInitialLoad = savedState == null;
        }

        /// <summary>
        /// Saves the appropriate state for the page.
        /// </summary>
        /// <returns>The saved data.</returns>
        protected override object SaveControlState()
        {
            bool isInitialLoad = false;
            return isInitialLoad;
        }

        /// <summary>
        /// Passes through workflow parameters as hidden fields.
        /// </summary>
        /// <param name="e">An <see cref="EventArgs"/> object
        /// representing event data.</param>
        protected override void OnPreRender(EventArgs e)
        {
            string[] passthroughParameters = 
            { 
                    "WorkflowName", "AddToStatusMenu", "AllowManual",
                    "RoleSelect", "AutoStartCreate", "AutoStartChange",
                    "GuidAssoc", "SetDefault", "HistoryList",
                    "TaskList", "UpdateLists", "WorkflowDefinition"
            };
            foreach (string parameter in passthroughParameters)
            {
                ClientScript.RegisterHiddenField(
                    parameter,
                    QueryStringParser.GetParameterAllowNull<String>(parameter));
            }
            base.OnPreRender(e);
        }
        #endregion

        #region [===== Association =====]
        /// <summary>
        /// Commits the association with the specified data and redirects to 
        /// the workflow settings page.
        /// </summary>
        /// <remarks>
        /// This method should be called for committing both new and existing
        /// associations. Custom association data can be passed in as a 
        /// <see cref="ConfigurationData" /> object.
        /// </remarks>
        /// <param name="data">The data to pass to the workflow as 
        /// association data.</param>
        /// <exception cref="ArgumentNullException">Thrown when the 
        /// <paramref name="data"/> is not specified.</exception>
        protected void CommitAssociation(ConfigurationData data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }
            using (SPLongOperation operation = new SPLongOperation(this))
            {
                operation.LeadingHTML = GetProgressHtml();
                operation.Begin();

                // Make sure the Tasks list exists
                // The id or name of the list is in the 
                // query string parameters
                SPList taskList = EnsureList(
                    TaskListID,
                    SPListTemplateType.Tasks);
                // Make sure the History list exists
                // The id or name of the list is in the 
                // query string parameters
                SPList historyList = EnsureList(
                    HistoryListID,
                    SPListTemplateType.WorkflowHistory);
                // Get the list which is going to have the 
                // workflow attached
                if (IsNewAssociation)
                {
                    CreateAssociation(data.NativeData, taskList, historyList);
                }
                else
                {
                    SPWorkflowAssociation assocation = WorkflowAssociationInternal;
                    UpdateAssocation(assocation, data.NativeData, taskList, historyList);                    
                }

                operation.End(BuildRedirectString("WrkSetng.aspx"), 
                    SPRedirectFlags.RelativeToLayoutsPage, this.Context, null);
            }
        }

        /// <summary>
        /// Cancels the association by redirecting to 
        /// the workflow settings page.
        /// </summary>
        protected void CancelAssociation()
        {
            string redirectString = BuildRedirectString("AddWrkfl.aspx");            
            if (!IsNewAssociation)
                redirectString += string.Format(CultureInfo.InvariantCulture, "&TemplateID={0}", 
                    SPHttpUtility.UrlKeyValueEncode(ExistingAssociationID.Value));

            SPUtility.Redirect(redirectString,
                SPRedirectFlags.RelativeToLayoutsPage, Context);
        }

        /// <summary>
        /// Gets the data from an existing workflow association.
        /// </summary>
        /// <returns>The data encapsulated in a 
        /// <see cref="ConfigurationData" /> object.</returns>
        protected ConfigurationData GetAssociationData()
        {
            string data = null;
            if (IsNewAssociation == false)
            {
                SPWorkflowAssociation association = WorkflowAssociationInternal;
                data = association.AssociationData;
            }
            return ConfigurationData.CreateUsingStringData(data);
        }

        /// <summary>
        /// Returns a metadata property from the workflow template.
        /// </summary>
        /// <param name="name">The name of the property to retrieve.</param>
        /// <returns>The value of the property.</returns>
        protected object GetWorkflowTemplateData(string name)
        {
            return WorkflowTemplateInternal[name];
        }

        /// <summary>
        /// Gets the HTML to display in the progress screen.
        /// </summary>
        /// <returns>A string of HTML, without the HTML, 
        /// BODY or FORM tags</returns>
        protected virtual string GetProgressHtml()
        {
            return null;
        }
        #endregion
       
        #endregion

        #region [===== Private methods =====]
        SPWorkflowAssociation GetExistingAssociation()
        {
            SPWorkflowAssociation association = null;
            switch (WorkflowAssociationType)
            {
                case AssociationType.List:
                    association = List.WorkflowAssociations[ExistingAssociationID.Value];
                    break;
                case AssociationType.ContentType:
                    association = SiteContentType.WorkflowAssociations[ExistingAssociationID.Value];
                    break;
                case AssociationType.ListContentType:
                    association = ListContentType.WorkflowAssociations[ExistingAssociationID.Value];
                    break;
            }
            return association;
        }

        void CreateAssociation(string data, SPList taskList, SPList historyList)
        {
            SPWorkflowTemplate workflowTemplate = WorkflowTemplateInternal;
            SPWorkflowAssociation association = null;
            switch (WorkflowAssociationType)
            {
                case AssociationType.List:
                    association = SPWorkflowAssociation.CreateListAssociation(
                        workflowTemplate, WorkflowName, taskList, historyList);
                    UpdateAssociationProperties(association, data, taskList, historyList);
                    List.AddWorkflowAssociation(association);
                    break;
                case AssociationType.ContentType:
                    association = SPWorkflowAssociation.CreateSiteContentTypeAssociation(
                        workflowTemplate, WorkflowName, taskList.Title, historyList.Title);
                    UpdateAssociationProperties(association, data, taskList, historyList);
                    SiteContentType.AddWorkflowAssociation(association);
                    if (CascadeContentTypeToList)
                    {
                        SiteContentType.UpdateWorkflowAssociationsOnChildren(true, true, true);
                    }
                    break;
                case AssociationType.ListContentType:
                    association = SPWorkflowAssociation.CreateListContentTypeAssociation(
                        workflowTemplate, WorkflowName, taskList, historyList);
                    UpdateAssociationProperties(association, data, taskList, historyList);
                    ListContentType.AddWorkflowAssociation(association);    
                    break;
            }
        }

        void UpdateAssocation(SPWorkflowAssociation assocation,
            string data, SPList taskList, SPList historyList)
        {
            UpdateAssociationProperties(assocation, data, taskList, historyList);
            switch (WorkflowAssociationType)
            {
                case AssociationType.List:
                    List.UpdateWorkflowAssociation(assocation);
                    break;
                case AssociationType.ContentType:
                    SiteContentType.UpdateWorkflowAssociation(assocation);
                    if (CascadeContentTypeToList)
                    {
                        SiteContentType.UpdateWorkflowAssociationsOnChildren(true, true, true);
                    }
                    break;
                case AssociationType.ListContentType:
                    ListContentType.UpdateWorkflowAssociation(assocation);
                    break;
            }
        }

        void UpdateAssociationProperties(
            SPWorkflowAssociation association, string data,
            SPList taskList, SPList historyList)
        {
            association.Name = WorkflowName;
            association.AutoStartCreate = AutoStartWorkflowOnCreate;
            association.AutoStartChange = AutoStartWorkflowOnChange;
            association.AllowManual = ManualStartWorkflow;

            if (association.TaskListId != taskList.ID)
            {
                association.SetTaskList(taskList);
            }
            if (association.HistoryListId != historyList.ID)
            {
                association.SetHistoryList(historyList);
            }
            association.AssociationData = data;
        }

        SPList EnsureList(string listIdentifier,
            SPListTemplateType listTemplateType)
        {
            SPList list = null;
            if (WorkflowAssociationType == AssociationType.List)
            {
                if (listIdentifier[0] != 'z')
                {
                    list = GetListByID(listIdentifier);
                }
                else
                {
                    list = EnsureNamedList(listIdentifier, listTemplateType);
                }
            }
            else
            {
                list = EnsureNamedList(listIdentifier, listTemplateType); ;
            }
            return list;
        }


        SPList GetListByID(string listIdentifier)
        {
            Guid listID = new Guid(listIdentifier);
            return Web.Lists[listID];
        }

        SPList EnsureNamedList(string listIdentifier, SPListTemplateType listTemplateType)
        {
            SPList list = null;
            try
            {
                list = Web.Lists[listIdentifier];
            }
            catch (ArgumentException)
            {
                string description = String.Format(CultureInfo.CurrentCulture, 
                    "Task list for the {0} workflow.", WorkflowName);
                Guid listID = Web.Lists.Add(
                    listIdentifier, description,
                    listTemplateType);
                list = Web.Lists[listID];
            }
            return list;
        }

        string BuildRedirectString(string pageUrl)
        {
            string redirect = null;
            switch (WorkflowAssociationType)
            {
                case AssociationType.List:
                    redirect = pageUrl + "?List=" + QueryStringParser.GetParameter<String>("List");
                    break;
                case AssociationType.ContentType:
                    redirect = pageUrl + "?ctype=" + QueryStringParser.GetParameter<String>("ctype");
                    break;
                case AssociationType.ListContentType:
                    redirect = pageUrl + "?List=" + QueryStringParser.GetParameter<String>("List") +
                        "&ctype=" + QueryStringParser.GetParameter<String>("ctype");
                    break;
            }
            return redirect;
        }
        #endregion
    }
}
